import hashlib
import logging
from itertools import izip
import mimetypes
import os
import random
import sys
import urllib

from google.appengine.ext import webapp
from google.appengine.ext.webapp.util import run_wsgi_app

from model import *

class SeedGetRight(webapp.RequestHandler):
    """
    Implement the "GetRight-style" bittorrent file-fetching-from-the-web protocol.
    See http://www.bittorrent.org/beps/bep_0019.html for specs.
    """

    def parse_range(self, length, range_header_string):
        """Parse HTTP byte-ranges; they look like:
           bytes=0-499
           bytes=-500
           bytes=500-
           bytes=500-599,700-799
           Returns list of tuples that are (start, length) (e.g. bytes=0-499,600-600 returns [(0,500) (600,1)] )
        """
        result = []
        (_, b) = range_header_string.split("=")
        for r in b.split(","):
            (start,end) = r.split("-")

            # According to rfc2616:
            # If the server ignores a byte-range-spec because it is syntactically
            # invalid, the server SHOULD treat the request as if the invalid Range
            # header field did not exist. (Normally, this means return a 200
            # response containing the full entity). 
            start = int(start) if start else 0
            end = int(end) if end else length-1
            if end < 0 or end >= length: end=length-1
            if start < 0 or start > end: start=0
            result.append( (start, end-start+1) )
        return result

    def get(self, hex_info_hash, path):
        path = urllib.unquote_plus(path) # Decode %20's or other weird chars in path
        logging.debug("Hash: %s, path: %s"%(hex_info_hash,path))
        t = Torrent.get_by_hash(hex_info_hash)
        if t is None:
            self.error(404) # Not Found
            return
        # Build a dictionary that maps path => (startbyte, length)
        if 'length' in t.info:
            path_dict = { t.info['name'] : (0, t.info['length']) }
        else:
            path_dict = { }
            start = 0
            for file in t.info['files']:
                full_path = os.path.join(t.info['name'], *file['path'])
                path_dict[full_path] = (start, file['length'])
                start += file['length']
        
        if path not in path_dict:
            self.error(404) # Not Found
            return

        # Guess type from filenames;
        (mimetype, encoding) = mimetypes.guess_type(path, False)
        if mimetype is None:
            mimetype = "application/octet-stream"

        (first_byte, length) = path_dict[path]

        # Figure out what bytes are being fetched:
        if 'Range' in self.request.headers:
            byte_ranges = self.parse_range(length, self.request.headers['Range'])
        else:
            byte_ranges = [ (0, length) ]
        logging.debug("Range(s) requested: "+str(byte_ranges))

        # Now fetch the right bytes from the TorrentPiece data store:
        byte_results = [ ]
        piece_length = t.info['piece length']
        for range in byte_ranges:
            data = t.get_data(range[0]+first_byte, range[1])
            byte_results.append(data)

        # Common case: only one range requested
        if len(byte_results) == 1:
            self.response.headers["Content-Type"] = mimetype
            if encoding is not None:
                self.response.headers["Content-Encoding"] = encoding
            if length != len(byte_results[0]):
                self.response.headers.add_header('Content-Range', 
                     'bytes %d-%d/%d'%(byte_ranges[0][0], byte_ranges[0][0]+byte_ranges[0][1]-1, length))
                self.response.set_status(206)  # Partial Content
            self.response.out.write(byte_results[0])
        # Uncommon case: multiple ranges.  Respond with multipart/byteranges content:
        else:
            boundary = hashlib.sha1("SomethingArbitrary%d"%(random.randint(10,1<<16),)).hexdigest()
            self.response.set_status(206)  # Partial Content
            self.response.headers['Content-Type'] = "multipart/byteranges; boundary="+boundary
            for (range, byte_result) in izip(byte_ranges, byte_results):
                self.response.out.write("--"+boundary+"\r\n")
                self.response.out.write("Content-Type: "+mimetype+"\r\n")
                if encoding is not None:
                    # Not sure if this is correct; encoding with each piece, or
                    # send Content-Encoding with the top-level headers?
                    logging.warning("Multi-range response with encoding "+encoding)
                    self.response.out.write("Content-Encoding: "+encoding+"\r\n")
                self.response.out.write("Content-Range: " + 
                     "bytes %d-%d/%d\r\n"%(range[0], range[0]+range[1]-1, length))
                self.response.out.write("\r\n")
                self.response.out.write(byte_result)
                self.response.out.write("\r\n")
            self.response.out.write("--"+boundary+"--\r\n")
                

application = webapp.WSGIApplication(
    [
      (r'.*?/([0-9a-f]{40})/(.*)', SeedGetRight)
    ],
    debug=True)

def main():
    run_wsgi_app(application)

if __name__ == "__main__":
    main()
